﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework;

namespace lumo.input
{
    /// <summary>
    /// Gamepad class.
    /// </summary>
    public class Gamepad : IUpdatableOnce
    {

        #region Enumerations

        /// <summary>
        /// Button enumeration
        /// </summary>
        public enum Button
        {
            /// <summary> The A Button </summary>
            A,

            /// <summary> The B Button </summary>
            B,

            /// <summary> The X Button </summary>
            X,

            /// <summary> The Y Button </summary>
            Y,

            /// <summary> Directional Up </summary>
            DUp,

            /// <summary> Directional Down </summary>
            DDown,

            /// <summary> Directional Left </summary>
            DLeft,

            /// <summary> Directional Right </summary>
            DRight,

            /// <summary> Left Stick </summary>
            LeftStick,

            /// <summary> Right Stick </summary>
            RightStick,

            /// <summary> Left Shoulder </summary>
            LeftShoulder,

            /// <summary> Right Shoulder </summary>
            RightShoulder
        }

        /// <summary>
        /// Thumbstick enumeration
        /// </summary>
        public enum Thumbstick
        {
            /// <summary> The Left Thumbstick </summary>
            Left,

            /// <summary> The Right Thumbstick </summary>
            Right
        }     

        /// <summary>
        /// Axis enumeration
        /// </summary>
        public enum Axis
        {
            /// <summary> Left X Axis </summary>
            LeftX,

            /// <summary> Left Y Axis </summary>
            LeftY,

            /// <summary> Right X Axis </summary>
            RightX,

            /// <summary> Right Y Axis </summary>
            RightY,

            /// <summary> Axis representing the triggers </summary>
            Triggers
        }  
        
        /// <summary>
        /// Triggers enumeration
        /// </summary>
        public enum Trigger
        {
            /// <summary> Left Axis to the Left </summary>
            LLeft,

            /// <summary> Left Axis to the Right </summary>
            LRight,

            /// <summary> Left Axis Up </summary>
            LUp,

            /// <summary> Left Axis Down </summary>
            LDown,

            /// <summary> Right Axis Left </summary>
            RLeft,

            /// <summary> Right Axis Right </summary>
            RRight,

            /// <summary> Right Axis Up </summary>
            RUp,

            /// <summary> Right Axis Down </summary>
            RDown,

            /// <summary> Left Trigger </summary>
            LTrigger,

            /// <summary> Right Trigger </summary>
            RTrigger
        }

        #endregion

        #region InputObject

        /// <summary>
        /// GamepadButton object
        /// </summary>
        abstract class GamepadInputObject : IInputObject
        {
            internal InputType InputType = InputType.None;
            internal long InputID = 0;
            internal Gamepad GamepadParent;
            private bool LastDown = false;
            private bool CurrentDown = false;
            private float RepeatCount = 0;
            private float _RepeatDelay = Config.DefaultRepeatDelay;
            private float _RepeatTime = Config.DefaultRepeatTime;

            // The propertie for frame ID
            private uint _FrameID = 0;
            /// <summary>Access for FrameID</summary>
            public uint FrameID { get { return _FrameID; } }

            /// <summary>
            /// Constructor for Input object.
            /// </summary>
            /// <param name="GamepadParent">The parent Gamepad</param>
            public GamepadInputObject(Gamepad GamepadParent)
            {
                this.GamepadParent = GamepadParent;
                Repeated = false;
            }

            /// <summary>
            /// Initialize the input status
            /// </summary>
            internal void InitializeStatus()
            {
                CurrentDown = LastDown = IsDown();
            }

            /// <summary>
            /// Check if it is down.
            /// </summary>
            /// <returns>True if it is down</returns>
            internal abstract bool IsDown();

            /// <summary>
            /// Update the key.
            /// </summary>
            /// <param name="Game">The game object</param>
            public void Update(LumoComponent Game)
            {
                // Only called once per frame.
                if (_FrameID == Game.FrameID)
                    return;
                _FrameID = Game.FrameID;
                GamepadParent.Update(Game);
            }

            /// <summary>
            /// Internal update called on Gamepad.Update
            /// </summary>
            internal virtual void InternalUpdate(LumoComponent Game)
            {
                LastDown = CurrentDown;
                CurrentDown = IsDown();
                RepeatUpdate(Game);
            }

            /// <summary>
            /// Internal Repeat Update
            /// </summary>
            private void RepeatUpdate(LumoComponent Game)
            {
                Repeated = false;
                if (Game == null)
                {
                    Repeated = Triggered;
                    return;
                }

                if (CurrentDown)
                {
                    if (!LastDown)
                    {
                        Repeated = true;
                        RepeatCount = _RepeatDelay;
                    }
                    else
                    {
                        RepeatCount -= (float)Game.GameTime.ElapsedGameTime.TotalMilliseconds;
                        if (RepeatCount <= 0)
                        {
                            Repeated = true;
                            RepeatCount = _RepeatTime;
                        }
                    }
                }
            }

            /// <summary>
            /// Type of object.
            /// </summary>
            /// <returns>The type of object.</returns>
            public InputType GetInputType() { return InputType; }

            /// <summary>
            /// Return the Button.
            /// </summary>
            /// <returns>The ID.</returns>
            public long GetInputID() { return InputID; }

            /// <summary>
            /// Property to check if key is pressed.
            /// </summary>
            public bool Pressed { get { return CurrentDown; } }

            /// <summary>
            /// Property to check if key is triggered.
            /// </summary>
            public bool Triggered { get { return (CurrentDown && !LastDown); } }

            /// <summary>
            /// Property to check if key is repeated.
            /// </summary>
            public bool Repeated { get; private set; }

            /// <summary>
            /// Property to check if key is released.
            /// </summary>
            public bool Released { get { return (LastDown && !CurrentDown); } }

            /// <summary>
            /// Implemented IInputObject Released.
            /// </summary>
            /// <returns>True if the key is released.</returns>
            public float RepeatDelay { get { return _RepeatDelay; } set { _RepeatDelay = value; } }

            /// <summary>
            /// Implemented IInputObject Released.
            /// </summary>
            /// <returns>True if the key is released.</returns>
            public float RepeatTime { get { return _RepeatTime; } set { _RepeatTime = value; } }
        }

        #endregion

        #region Button

        /// <summary>
        /// GamepadButton object
        /// </summary>
        class GamepadButton : GamepadInputObject
        {
            private Buttons Button;

            /// <summary>
            /// Constructor for key.
            /// </summary>
            /// <param name="GamepadParent">The parent Gamepad</param>
            /// <param name="Button">The Button</param>
            public GamepadButton(Gamepad GamepadParent, Buttons Button)
                : base(GamepadParent)
            {
                this.GamepadParent = GamepadParent;
                this.InputType = InputType.Button;
                this.InputID = (long)Button;
                this.Button = Button;
                InitializeStatus();
            }

            /// <summary>
            /// Check if the key is down.
            /// </summary>
            /// <returns>True if it is down</returns>
            internal override bool IsDown() { return this.GamepadParent.State.IsButtonDown(this.Button); }
        }

        #endregion

        #region Trigger

        /// <summary>
        /// Gamepad Trigger Object
        /// </summary>
        class GamepadTrigger : GamepadInputObject, ITrigger
        {
            // The function
            private Func<GamePadState, float> Function;

            /// <summary>
            /// The value
            /// </summary>
            public float Value { get; private set; }

            /// <summary>
            /// Constructor for key.
            /// </summary>
            /// <param name="GamepadParent">The parent Gamepad</param>
            /// <param name="Function">Lambda to get Trigger value</param>
            public GamepadTrigger(Gamepad GamepadParent, Func<GamePadState, float> Function) : base(GamepadParent)
            {
                this.Function = Function;
                this.Value = Function(GamepadParent.State);
                InitializeStatus();
            }

            /// <summary>
            /// Internal update called on Gamepad.Update
            /// </summary>
            internal override void InternalUpdate(LumoComponent Game)
            {
                this.Value = Function(GamepadParent.State);
                base.InternalUpdate(Game);
            }

            /// <summary>
            /// Check if the key is down.
            /// </summary>
            /// <returns>True if it is down</returns>
            internal override bool IsDown() { return (Math.Abs(this.Value) >= 0.5f); }
        }

        #endregion

        #region Axis

        /// <summary>
        /// Gamepad Axis Object
        /// </summary>
        class GamepadAxis : GamepadInputObject, IAxis
        {
            // Private variables
            private Func<GamePadState, float> Function;
            private float _Value = 0;

            /// <summary>
            /// The value
            /// </summary>
            public float Value { get { return (this.Reverse ? -_Value : _Value); } }

            /// <summary>
            /// Reverse?
            /// </summary>
            public bool Reverse { get; set; }

            /// <summary>
            /// Constructor for key.
            /// </summary>
            /// <param name="GamepadParent">The parent Gamepad</param>
            /// <param name="Function">Lambda to get Trigger value</param>
            public GamepadAxis(Gamepad GamepadParent, Func<GamePadState, float> Function) : base(GamepadParent)
            {
                this.Function = Function;
                this._Value = Function(GamepadParent.State);
                this.Reverse = false;
                InitializeStatus();
            }

            /// <summary>
            /// Internal update called on Gamepad.Update
            /// </summary>
            internal override void InternalUpdate(LumoComponent Game)
            {
                this._Value = Function(GamepadParent.State);
                base.InternalUpdate(Game);
            }

            /// <summary>
            /// Check if the key is down.
            /// </summary>
            /// <returns>True if it is down</returns>
            internal override bool IsDown() { return (Math.Abs(this._Value) >= 0.5f); }
        }

        #endregion

        #region Thumbstick

        /// <summary>
        /// Gamepad Axis Object
        /// </summary>
        class GamepadThumbstick : GamepadInputObject, IThumbstick
        {
            // Private variables
            private Func<GamePadState, Vector2> Function;
            private Vector2 _Vector = Vector2.Zero;

            /// <summary>
            /// The value
            /// </summary>
            public Vector2 Vector { get { return new Vector2((this.ReverseX ? -_Vector.X : _Vector.X), (this.ReverseY ? -_Vector.Y : _Vector.Y)); } }

            /// <summary>
            /// Reverse?
            /// </summary>
            public bool ReverseX { get; set; }

            /// <summary>
            /// Reverse?
            /// </summary>
            public bool ReverseY { get; set; }

            /// <summary>
            /// Constructor for thumbstick
            /// </summary>
            /// <param name="GamepadParent">The parent Gamepad</param>
            /// <param name="Function">Lambda to get Thumbstick value</param>
            public GamepadThumbstick(Gamepad GamepadParent, Func<GamePadState, Vector2> Function) : base(GamepadParent)
            {
                this.Function      = Function;
                this._Vector        = Function(GamepadParent.State);
                this.ReverseX      = false;
                this.ReverseY      = false;
                InitializeStatus();
            }

            /// <summary>
            /// Internal update called on Gamepad.Update
            /// </summary>
            internal override void InternalUpdate(LumoComponent Game)
            {
                this._Vector = Function(GamepadParent.State);
                base.InternalUpdate(Game);
            }

            /// <summary>
            /// Check if the key is down.
            /// </summary>
            /// <returns>True if it is down</returns>
            internal override bool IsDown() { return (Math.Abs(this._Vector.Length()) >= 0.5f); }
        }

        #endregion

        // Private variables
        private Dictionary<Button,  GamepadButton>  GamepadButtonDictionary  = new Dictionary<Button, GamepadButton>();
        private Dictionary<Trigger, GamepadTrigger> GamepadTriggerDictionary = new Dictionary<Trigger, GamepadTrigger>();
        private Dictionary<Axis,    GamepadAxis> GamepadAxisDictionary = new Dictionary<Axis, GamepadAxis>();
        private Dictionary<Thumbstick, GamepadThumbstick> GamepadThumbstickDictionary = new Dictionary<Thumbstick, GamepadThumbstick>();
        private GamePadState State;
        private PlayerIndex  PlayerIndex;

        // The propertie for frame ID
        private uint _FrameID = 0;
        /// <summary>Access for FrameID</summary>
        public uint FrameID { get { return _FrameID; } }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="PlayerIndex">Index of the player.</param>
        public Gamepad(PlayerIndex PlayerIndex)
        {
            // Save the index
            this.PlayerIndex = PlayerIndex;
            UpdateState();

            // Create the buttons
            GamepadButtonDictionary.Add(Button.A, new GamepadButton(this, Microsoft.Xna.Framework.Input.Buttons.A));
            GamepadButtonDictionary.Add(Button.B, new GamepadButton(this, Microsoft.Xna.Framework.Input.Buttons.B));
            GamepadButtonDictionary.Add(Button.X, new GamepadButton(this, Microsoft.Xna.Framework.Input.Buttons.X));
            GamepadButtonDictionary.Add(Button.Y, new GamepadButton(this, Microsoft.Xna.Framework.Input.Buttons.Y));
            // Directional Buttons
            GamepadButtonDictionary.Add(Button.DUp,    new GamepadButton(this, Microsoft.Xna.Framework.Input.Buttons.DPadUp));
            GamepadButtonDictionary.Add(Button.DDown,  new GamepadButton(this, Microsoft.Xna.Framework.Input.Buttons.DPadDown));
            GamepadButtonDictionary.Add(Button.DLeft,  new GamepadButton(this, Microsoft.Xna.Framework.Input.Buttons.DPadLeft));
            GamepadButtonDictionary.Add(Button.DRight, new GamepadButton(this, Microsoft.Xna.Framework.Input.Buttons.DPadRight));
            // Sticks
            GamepadButtonDictionary.Add(Button.LeftStick,  new GamepadButton(this, Microsoft.Xna.Framework.Input.Buttons.LeftStick));
            GamepadButtonDictionary.Add(Button.RightStick, new GamepadButton(this, Microsoft.Xna.Framework.Input.Buttons.RightStick));
            // Shoulders
            GamepadButtonDictionary.Add(Button.LeftShoulder,  new GamepadButton(this, Microsoft.Xna.Framework.Input.Buttons.LeftShoulder));
            GamepadButtonDictionary.Add(Button.RightShoulder, new GamepadButton(this, Microsoft.Xna.Framework.Input.Buttons.RightShoulder));

            // Create the Triggers
            GamepadTriggerDictionary.Add(Trigger.LRight,   new GamepadTrigger(this, state => MathHelper.Clamp(state.ThumbSticks.Left.X, 0, 1)));
            GamepadTriggerDictionary.Add(Trigger.LLeft,    new GamepadTrigger(this, state => Math.Abs(MathHelper.Clamp(state.ThumbSticks.Left.X, -1, 0))));
            GamepadTriggerDictionary.Add(Trigger.LUp,      new GamepadTrigger(this, state => MathHelper.Clamp(state.ThumbSticks.Left.Y, 0, 1)));
            GamepadTriggerDictionary.Add(Trigger.LDown,    new GamepadTrigger(this, state => Math.Abs(MathHelper.Clamp(state.ThumbSticks.Left.Y, -1, 0))));
            GamepadTriggerDictionary.Add(Trigger.RRight,   new GamepadTrigger(this, state => MathHelper.Clamp(state.ThumbSticks.Right.X, 0, 1)));
            GamepadTriggerDictionary.Add(Trigger.RLeft,    new GamepadTrigger(this, state => Math.Abs(MathHelper.Clamp(state.ThumbSticks.Right.X, -1, 0))));
            GamepadTriggerDictionary.Add(Trigger.RUp,      new GamepadTrigger(this, state => MathHelper.Clamp(state.ThumbSticks.Right.Y, 0, 1)));
            GamepadTriggerDictionary.Add(Trigger.RDown,    new GamepadTrigger(this, state => Math.Abs(MathHelper.Clamp(state.ThumbSticks.Right.Y, -1, 0))));
            GamepadTriggerDictionary.Add(Trigger.LTrigger, new GamepadTrigger(this, state => MathHelper.Clamp(state.Triggers.Left, 0, 1)));
            GamepadTriggerDictionary.Add(Trigger.RTrigger, new GamepadTrigger(this, state => MathHelper.Clamp(state.Triggers.Right, 0, 1)));

            // Create the axes
            GamepadAxisDictionary.Add(Axis.LeftX, new GamepadAxis(this, state => MathHelper.Clamp(state.ThumbSticks.Left.X, -1, 1)));
            GamepadAxisDictionary.Add(Axis.LeftY, new GamepadAxis(this, state => MathHelper.Clamp(state.ThumbSticks.Left.Y, -1, 1)));
            GamepadAxisDictionary.Add(Axis.RightX, new GamepadAxis(this, state => MathHelper.Clamp(state.ThumbSticks.Right.X, -1, 1)));
            GamepadAxisDictionary.Add(Axis.RightY, new GamepadAxis(this, state => MathHelper.Clamp(state.ThumbSticks.Right.Y, -1, 1)));
            GamepadAxisDictionary.Add(Axis.Triggers, new GamepadAxis(this, state => MathHelper.Clamp(state.Triggers.Right - state.Triggers.Left, -1, 1)));

            // Create the thumbsticks
            GamepadThumbstickDictionary.Add(Thumbstick.Left, new GamepadThumbstick(this, state => state.ThumbSticks.Left));
            GamepadThumbstickDictionary.Add(Thumbstick.Right, new GamepadThumbstick(this, state => state.ThumbSticks.Right));
        }

        /// <summary>
        /// Update the state of the gamepad.
        /// </summary>
        private void UpdateState()
        {
            State = GamePad.GetState(PlayerIndex);
        }

        /// <summary>
        /// Update the current Gamepad.
        /// </summary>
        /// <param name="Button">The button to get.</param>
        public IInputObject GetButton(Button Button)
        {
            if (!GamepadButtonDictionary.ContainsKey(Button))
                throw new Exception("Invalid button");
            return GamepadButtonDictionary[Button];
        }

        /// <summary>
        /// Get all buttons.
        /// </summary>
        public IEnumerable<IInputObject> GetButtons()
        {
            return GamepadButtonDictionary.Values;
        }

        /// <summary>
        /// Get a single Trigger.
        /// </summary>
        /// <param name="Trigger">The Trigger to get.</param>
        public ITrigger GetTrigger(Trigger Trigger)
        {
            if (!GamepadTriggerDictionary.ContainsKey(Trigger))
                throw new Exception("Invalid button");
            return GamepadTriggerDictionary[Trigger];
        }

        /// <summary>
        /// Get all triggers.
        /// </summary>
        public IEnumerable<ITrigger> GetTriggers()
        {
            return GamepadTriggerDictionary.Values;
        }

        /// <summary>
        /// Get a single Axis
        /// </summary>
        /// <param name="Axis">The Axis to get.</param>
        public IAxis GetAxis(Axis Axis)
        {
            if (!GamepadAxisDictionary.ContainsKey(Axis))
                throw new Exception("Invalid button");
            return GamepadAxisDictionary[Axis];
        }

        /// <summary>
        /// Get all axes.
        /// </summary>
        public IEnumerable<IAxis> GetAxes()
        {
            return GamepadAxisDictionary.Values;
        }

        /// <summary>
        /// Get a single Thumbstick
        /// </summary>
        /// <param name="Thumbstick">The Thumbstick to get.</param>
        public IThumbstick GetThumbstick(Thumbstick Thumbstick)
        {
            if (!GamepadThumbstickDictionary.ContainsKey(Thumbstick))
                throw new Exception("Invalid button");
            return GamepadThumbstickDictionary[Thumbstick];
        }

        /// <summary>
        /// Get all Thumbsticks.
        /// </summary>
        public IEnumerable<IThumbstick> GetThumbsticks()
        {
            return GamepadThumbstickDictionary.Values;
        }

        /// <summary>
        /// Update the current Gamepad.
        /// </summary>
        /// <param name="Game">The game object.</param>
        public void Update(lumo.LumoComponent Game)
        {
            // Check for already updated object
            if (_FrameID == Game.FrameID)
                return;
            _FrameID = Game.FrameID;

            // Save the state.
            UpdateState();

            // Update every key
            foreach (GamepadButton btn in GamepadButtonDictionary.Values)
            {
                btn.InternalUpdate(Game);
            }

            // Update every key
            foreach (GamepadTrigger trigger in GamepadTriggerDictionary.Values)
            {
                trigger.InternalUpdate(Game);
            }

            // Update every key
            foreach (GamepadAxis axis in GamepadAxisDictionary.Values)
            {
                axis.InternalUpdate(Game);
            }
        }
    }
}
